﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using Skugo.Shared;
using System.IO;
using System.Diagnostics;

namespace Skugo
{
    /// <summary>
    /// A target is a root object and root property node paired together.
    /// </summary>
    public struct LocalTarget
    {
        public Object TargetObject;
        public LocalPropertyNode Root;
        public LocalPropertyNode[] FlattenedHierarchy;
    }

    // A property grid is a UI control that shows all editable properties of selected object(s)
    public class PropertyGrid : DockablePlugin
    {
        /// <summary>
        /// The name of the remote type we wish to instantiate for our plugin.
        /// </summary>
        private const String RemoteType = "Skugo.StandardPlugins.PropertyGrid";

        /// <summary>
        /// The type of message we send or receieve.
        /// </summary>
        enum MessageType : byte
        {
            /// <summary>
            /// A snapshot of all trees (their types, children, etc).
            /// </summary>
            TreeSnapshot,

            /// <summary>
            /// A snapshot of all properties.
            /// </summary>
            PropertySnapshot,

            /// <summary>
            /// Set a particular property on the property grid.
            /// </summary>
            SetLocalProperty,

            /// <summary>
            /// Call a local method on the objects bound to the property grid.
            /// </summary>
            CallLocalMethod,

            /// <summary>
            /// Tells us whether the client is ready for the property snapshot.
            /// </summary>
            ReadyForPropertySnapshot,
        }

        private Boolean IsReadyForSnapshot = false;

        private IEnumerable<Object> Objects = null;

        private List<LocalTarget> Targets = null;

        private IPropertyProvider Provider = null;

        private GeneralPropertyProvider GeneralProvider = new GeneralPropertyProvider();

        private struct AccessedMember
        {
            public LocalPropertyNode Node;
            public Object Owner;
        }


        private static Dictionary<MemberInfo, Boolean> MemberIsProperty = new Dictionary<MemberInfo, Boolean>();

        public PropertyGrid(Client client, String displayName) :
            base(RemoteType, client, displayName)
        {
            // We want to know when the framework gets updated, so we can refresh the properties / tree
            Framework.Instance.OnUpdate += this.OnUpdate;
        }

        /// <summary>
        /// Updating the tree involves querying for a new tree, comparing to see
        /// if they are different and if so resetting and sending the new tree over.
        /// </summary>
        public void UpdateTree()
        {
            // Create a new list of targets
            var newTargets = new List<LocalTarget>();

            // For all objects that are selected...
            foreach (var targetObject in this.Objects)
            {
                // Create an object to hold all the information we pass into the provider interface
                var info = new PropertyProviderInfo()
                {
                    RootObject = targetObject,
                    IsMemberValid = PropertyMembersOnly,
                };

                // Build the property tree for the given object
                var root = this.Provider.GetPropertyTree(info);

                // Add the targets to the 
                newTargets.Add(new LocalTarget()
                {
                    FlattenedHierarchy = FlattenTree(root),
                    Root = root,
                    TargetObject = targetObject,
                });
            }

            // Save the old targets
            var oldTargets = this.Targets;

            // Even if the trees are not different, the targets could
            // still be different (identically structured objects)
            this.Targets = newTargets;

            // If the trees were different at all...
            if (AreTargetsTheSame(oldTargets, this.Targets) == false)
            {
                // Send the tree to everyone
                this.SendTrees();
            }

            // Send the property snapshot, regardless of whether or not we're ready
            this.SendPropertySnapshot();
        }


        /// <summary>
        /// Sends a complete snapshot of all trees to the given writer.
        /// </summary>
        public void SendTrees()
        {
            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write out the tree definition
            outgoing.Write((Byte)MessageType.TreeSnapshot);
            outgoing.Write((Int32)this.Targets.Count);

            // Loop through the targets
            foreach (var target in this.Targets)
            {
                // Write out the tree for the given target
                WriteChildrenRecursive(outgoing, target.Root);
            }

            // Send the rpc data
            this.Send(stream.ToArray());
        }


        /// <summary>
        /// Write out the tree by basically just going through the flattened hierarchy and writing each node.
        /// </summary>
        /// <param name="outgoing">The writer we are writing the tree to.</param>
        /// <param name="node">The node that is to be written.</param>
        private static void WriteChildrenRecursive(BinaryWriter outgoing, LocalPropertyNode parent)
        {
            // Write out the number of child nodes
            outgoing.Write(parent.Children.Count);

            // Loop through all the child nodes
            foreach (var child in parent.Children)
            {
                var info = NodeOptions.None;

                if (child.OptionalName != null)
                    info |= NodeOptions.Name;

                if (child.OptionalDescription != null)
                    info |= NodeOptions.Description;

                if (child.OptionalCategory != null)
                    info |= NodeOptions.Category;

                if (child.DefaultExpanded)
                    info |= NodeOptions.DefaultExpanded;

                outgoing.Write((Byte)info);

                // If the node has a name...
                if (child.OptionalName != null)
                {
                    // Write out that the node is named, and then write the name
                    outgoing.WriteAsciiString(child.OptionalName);
                }

                // If the node has a description...
                if (child.OptionalDescription != null)
                {
                    // Write out that the node is named, and then write the name
                    outgoing.WriteAsciiString(child.OptionalDescription);
                }

                // If the node has a category...
                if (child.OptionalCategory != null)
                {
                    // Write out that the node is named, and then write the name
                    outgoing.WriteAsciiString(child.OptionalCategory);
                }

                // Write out the type of node this is
                outgoing.Write((Byte)child.Kind);

                // If the node is a method node
                switch (child.Kind)
                {
                    case NodeKind.Method:
                    {
                        // Get all the parameters from the method
                        var parameters = child.Method.GetParameters();

                        // Write out the parameter count
                        outgoing.Write((Byte)parameters.Length);

                        // Now loop through all the parameters
                        foreach (var parameter in parameters)
                        {
                            // Write out the guid for the parameter type
                            outgoing.Write(TypeFactory.GetInfoFromPrimitiveType(parameter.ParameterType).Guid);
                        }
                        break;
                    }

                    case NodeKind.Expandable:
                    {
                        // Write out all children of the tree
                        WriteChildrenRecursive(outgoing, child);
                        break;
                    }

                    case NodeKind.Primitive:
                    {
                        // Write out the guid for the primitive type
                        // We always assume that primitives have no children, and therefore there's nothing to write
                        outgoing.Write(TypeFactory.GetInfoFromPrimitiveType(child.Type).Guid);
                        break;
                    }

                    case NodeKind.Enumerable:
                    {
                        // Write out the number of options this node has
                        outgoing.Write(child.EnumeratedOptions.Length);

                        // Loop through all the enumerated options and write them out
                        foreach (var option in child.EnumeratedOptions)
                        {
                            // Write out each option
                            outgoing.WriteAsciiString(option);
                        }
                        break;
                    }
                }
            }
        }


        /// <summary>
        /// Compare two lists of rpc targets to see if they are the same.
        /// </summary>
        /// <param name="targets1">The first targets list.</param>
        /// <param name="targets2">The second targets list.</param>
        /// <returns>True if the lists represent the same tree structure, false otherwise.</returns>
        private static Boolean AreTargetsTheSame(List<LocalTarget> targets1, List<LocalTarget> targets2)
        {
            // If the two targets are the same, then they of course are the same (this will also handle the null case
            if (targets1 == targets2)
                return true;

            // Check if one tree or the other tree was null (note they cannot both be null due to the above case)
            if (targets1 == null || targets2 == null)
            {
                // One tree did not exist, so of course its different!
                return false;
            }
            else
            {
                // If the number of built targets is different
                if (targets1.Count != targets2.Count)
                {
                    // The target count was different, therefore the trees were different
                    return false;
                }
                else
                {
                    // Loop through all the targets
                    for (Int32 i = 0; i < targets1.Count; ++i)
                    {
                        // Get the new and old hierarchies for convenience
                        var newHierarchy = targets1[i].FlattenedHierarchy;
                        var oldHierarchy = targets2[i].FlattenedHierarchy;

                        // If the hierarchies differ in the number of nodes... then the trees are different
                        if (newHierarchy.Length != oldHierarchy.Length)
                        {
                            // Return early since they were different
                            return false;
                        }

                        // Now loop through all nodes in the flattened hierarchy
                        for (Int32 j = 0; j < newHierarchy.Length; ++j)
                        {
                            // If the two compared nodes were not the same...
                            if (newHierarchy[j].Equals(oldHierarchy[j]) == false)
                            {
                                // Return early since they were different
                                return false;
                            }
                        }
                    }
                }
            }

            // Otherwise, if we got here, they must be the same
            return true;
        }

        /// <summary>
        /// Flatten a property tree into a single array.
        /// </summary>
        /// <param name="root">The root of the tree.</param>
        /// <returns>Returns the flattened array of tree nodes.</returns>
        private static LocalPropertyNode[] FlattenTree(LocalPropertyNode root)
        {
            // Create a list for the flattened tree (where we store all the nodes)
            var flat = new List<LocalPropertyNode>();

            // Recursively flatten the tree
            FlattenTreeRecursive(root, flat);

            // Return the flattened array
            return flat.ToArray();
        }

        /// <summary>
        /// Recursively flatten a property tree branch by branch.
        /// </summary>
        /// <param name="branch">The current branch that we're flattening.</param>
        /// <param name="flat">The list of all nodes that we're flattening into.</param>
        private static void FlattenTreeRecursive(LocalPropertyNode branch, List<LocalPropertyNode> flat)
        {
            // Loop through all the children
            foreach (var child in branch.Children)
            {
                // Add the child to the flattened hierarchy
                flat.Add(child);

                // Flatten the tree recursively
                FlattenTreeRecursive(child, flat);
            }
        }

        private void OnUpdate()
        {
            // If we have no objects, return early
            if (this.Objects == null)
            {
                return;
            }

            // Update the tree (which could potentailly send out a new tree)
            this.UpdateTree();

            if (this.IsReadyForSnapshot)
            {
                // Send the property snapshot
                this.SendPropertySnapshot();
            }
        }


        /// <summary>
        /// Sends a complete property value snapshot to the given writer.
        /// </summary>
        public void SendPropertySnapshot()
        {
            // We are no longer ready to send another snapshot
            this.IsReadyForSnapshot = false;

            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write out a snapshot of all properties
            outgoing.Write((Byte)MessageType.PropertySnapshot);

            // Loop through the targets
            foreach (var target in this.Targets)
            {
                // Write out the property snapshot. Note that it actually traverses down the tree
                // and requires a root (which should always be the first node in the hierarchy)
                WritePropertySnapshot(outgoing, target.TargetObject, target.Root);
            }

            // Send the rpc data
            this.Send(stream.ToArray());
        }

        /// <summary>
        /// Perform a full write of all properties to the given binary writer. Note that this call does recurse through the tree
        /// instead of just sampling the flattened hierarchy, because the process of getting each value requeires knowing the parent
        /// object (which could change regardless of the tree structure). We write it out in flattened (breadth first) order.
        /// </summary>
        /// <param name="outgoing">The writer we'd like to write the snapshot to.</param>
        /// <param name="owner">The root object (or the parent object of the node that is being written).</param>
        /// <param name="parent">The node that is to be written.</param>
        private static void WritePropertySnapshot(BinaryWriter outgoing, Object owner, LocalPropertyNode parent)
        {
            // Loop through all the child nodes
            foreach (var child in parent.Children)
            {
                // Get the current child object
                var ownerOrValue = child.Getter(owner);

                // If the child is a primitive type...
                if (child.Kind == NodeKind.Primitive)
                {
                    // Get the type information from the node's type
                    var typeInfo = TypeFactory.GetInfoFromPrimitiveType(child.Type);

                    // Write the binary representation of the object to the writer
                    typeInfo.ToBinary(outgoing, ownerOrValue);
                }
                else if (child.Kind == NodeKind.Enumerable)
                {
                    // Get the type information from the node's type
                    var typeInfo = TypeFactory.GetInfoFromEnumerableType(child.Type);

                    // Get the enumerable index, which is what we'll send to the other side
                    var index = typeInfo.ToIndex(ownerOrValue);

                    // Write out the index
                    outgoing.Write(index);
                }
                else if (child.Kind == NodeKind.Expandable)
                {
                    // Continue down and write this child's children
                    WritePropertySnapshot(outgoing, ownerOrValue, child);
                }
            }
        }

        // Make sure that we only use members marked with a Property attribute
        public static Boolean PropertyMembersOnly(MemberInfo member)
        {
            Boolean isMember;
            if (MemberIsProperty.TryGetValue(member, out isMember))
            {
                return isMember;
            }
            else
            {
                // Check if they have the property attribute
                isMember = (member.GetCustomAttributes(typeof(PropertyAttribute), false).Length != 0);
                MemberIsProperty.Add(member, isMember);
                return isMember;
            }
        }

        public void ClearSelection()
        {
            // Clear the objects and the provider
            this.Objects = null;
            this.Provider = null;
        }

        private void UpdateSelection(Boolean shouldClear, IPropertyProvider provider, IEnumerable<Object> objects)
        {
            // If it's marked that we should clear the selection (typically because the user passed in null)
            if (shouldClear)
            {
                // Clear the selection and return
                this.ClearSelection();
                return;
            }

            // If the provider given was null
            if (provider == null)
            {
                // Set the provider to the general provider
                provider = this.GeneralProvider;
            }

            // Set the objects and the provider that we're going to use to display a property grid
            this.Objects = objects;
            this.Provider = provider;

            // Update the tree (which could potentially send out a new tree)
            this.UpdateTree();
        }

        public void SetSelection(Object value)
        {
            this.UpdateSelection(value == null, null, value.AsEnumerable());
        }

        public void SetSelection(IEnumerable<Object> values)
        {
            this.UpdateSelection(values == null, null, values);
        }

        public void SetSelection(Object value, IPropertyProvider propertyProvider)
        {
            this.UpdateSelection(value == null, propertyProvider, value.AsEnumerable());
        }

        public void SetSelection(IEnumerable<Object> values, IPropertyProvider propertyProvider)
        {
            this.UpdateSelection(values == null, propertyProvider, values);
        }

        protected override void OnReceive(Byte[] data)
        {
            // Create a reader to read the data
            var incoming = data.ToBinaryReader();

            // Read the message type in
            var messageType = (MessageType)incoming.ReadByte();

            // Based on the message type...
            switch (messageType)
            {
                case MessageType.ReadyForPropertySnapshot:
                {
                    this.IsReadyForSnapshot = true;
                    break;
                }

                case MessageType.SetLocalProperty:
                {
                    //TODO We need to make a tree id, so we never do invalid sets

                    // Read each member index as we go
                    var memberIndex = incoming.ReadUInt32();
                    var targetIndex = incoming.ReadUInt32();

                    // Get the member in the tree using the member index for the given target
                    var member = this.GetMember(memberIndex, this.Targets[(Int32)targetIndex]);

                    // The value we'll be reading below
                    Object newValue;

                    // If the node's type is a primitive
                    if (member.Node.Kind == NodeKind.Primitive)
                    {
                        // Grab information about the member's primitive type
                        var typeInfo = TypeFactory.GetInfoFromPrimitiveType(member.Node.Type);

                        // Read in the new/changed value from the binary stream
                        newValue = typeInfo.FromBinary(incoming);
                    }
                    else if (member.Node.Kind == NodeKind.Enumerable)
                    {
                        // Grab information about the member's enumerable type
                        var typeInfo = TypeFactory.GetInfoFromEnumerableType(member.Node.Type);

                        // Read the enumerable index
                        var index = incoming.ReadInt32();

                        // Get the actual value from the given index
                        newValue = typeInfo.FromIndex(index);
                    }
                    else
                    {
                        throw new Exception("Attempt to set a node that wasn't primitive or enumerable");
                    }

                    // Finally, set the value of that member to the new value
                    member.Node.Setter(member.Owner, newValue);
                    break;
                }

                default:
                    throw new Exception("Unhandled message type!");
            }
        }

        //private void DoRpcAction(BinaryReader incoming, MessageType type, UInt32 memberIndex)
        //{
        //    // Get the corresponding member that was accessed for this tree
        //    // Note that this access MUST be valid for all trees on all targets
        //    var access = this.GetMember(memberIndex, target);

        //    // If the node is a method node (we have a method associated with it)
        //    if (access.Node.Method != null)
        //    {
        //        // Error checking
        //        if (type != RpcMessageType.Call)
        //        {
        //            throw new Exception("An attempted Rpc Set was made to a method");
        //        }

        //        // Read the number of parameters to the method call
        //        var numParameters = incoming.ReadByte();

        //        // Create an array of objects to store the parameters (so we can invoke the method)
        //        var parameters = new Object[numParameters];

        //        // Read all the parameters
        //        for (int i = 0; i < numParameters; ++i)
        //        {
        //            // Read the current parameter
        //            parameters[i] = incoming.ReadDynamic();
        //        }

        //        // Invoke the method with the given parameters
        //        access.Node.Method.Invoke(access.Owner, parameters);
        //    }
        //    // Otherwise, we just assume that it's either a value or object
        //    else
        //    {
        //        // Error checking
        //        if (type != RpcMessageType.Set)
        //        {
        //            throw new Exception("An attempted Rpc Call was made to a field or property member");
        //        }

        //        // Read the value that we want to use
        //        var value = incoming.ReadDynamic();

        //        // Set the value to a value that we dynamically read/create from the stream
        //        access.Node.Setter(access.Owner, value);
        //    }
        //}

        /// <summary>
        /// Get a member of the given member index from the rpc target.
        /// </summary>
        /// <param name="memberIndex">The member index that the member should exist at.</param>
        /// <param name="target">The target contains the root object and flattened heirarchy of nodes.</param>
        /// <returns>Returns the owner and the associated node.</returns>
        private AccessedMember GetMember(UInt32 memberIndex, LocalTarget target)
        {
            // Get the node associated with the member index from the flattened heirarchy
            var node = target.FlattenedHierarchy[memberIndex];

            // Use a recursive call to walk up the parents, then walk back down and get each object
            // along the way (the final one would result in the owner of the current node)
            var owner = this.GetMemberOwnerRecursive(node.Parent, target.TargetObject);

            // Returned the 'accessed member' which tells us which node and relative owner was involved
            return new AccessedMember()
            {
                Node = node,
                Owner = owner,
            };
        }

        /// <summary>
        /// A recursive function that we call to get the owner of a given node. Note that this call
        /// must walk up the heirarchy to the root, then use the root object to walk back down getting
        /// each sub owner as we go, until we reach the owner who matches the node.
        /// </summary>
        /// <param name="node">The node who we'd like to traverse all owners, starting from the root.</param>
        /// <param name="root">The root object in the tree.</param>
        /// <returns>The owner for the given node.</returns>
        private Object GetMemberOwnerRecursive(LocalPropertyNode node, Object root)
        {
            // If we have no parent (base case)
            if (node.Parent == null)
            {
                // Then return the root object
                return root;
            }
            else
            {
                // Otherwise, traverse up then back down getting each owner as we go
                return node.Getter(this.GetMemberOwnerRecursive(node.Parent, root));
            }
        }
    }

    /// <summary>
    /// A property attribute marks which properties are to be shown in the property grid.
    /// </summary>
    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field)]
    public class PropertyAttribute : Attribute
    {
        /// <summary>
        /// The category we'd like to place the property in (for organization).
        /// </summary>
        public String Category;

        /// <summary>
        /// A basic description of the property.
        /// </summary>
        public String Description;

        /// <summary>
        /// The type of editor we should use for the property (eg a Color editor).
        /// </summary>
        public Type PropertyEditorType;

        /// <summary>
        /// Wether or not this property is defaultly expanded. The default is true.
        /// </summary>
        public Boolean Expanded = true;
    }

    /// <summary>
    /// A property attribute marks which properties are to be shown in the property grid.
    /// </summary>
    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field)]
    public class NullableAttribute : Attribute
    {
    }
}